iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 17 篇

# Day 17 - 視覺與互動微調:運用 AI 開發 CSS 動畫與微互動(Framer Motion)

  • 分享至 

  • xImage
  •  

在完成了 API 串接與圖表繪製後,我們的 AI 智慧個人財務追蹤器 (Smart Finance Tracker) 已經具備了完整的骨架與肌肉。但要讓使用者感覺這是一款「現代、靈動且有質感」的產品,還差最後臨門一腳 —— 微互動 (Micro-interactions) 與流暢動畫。今天我們引入 React 生態系最強大的動畫庫 Framer Motion!

今日目標

為財務面板(Dashboard)中的卡片(Stat Cards)與交易列表加入進場淡入與滑入動畫(Entrance Animations)。

為新增交易按鈕與互動元件加入微互動(Hover, Tap Scale Feedback)。

解決 AI 在處理 Framer Motion layout 動畫與動態列表刪除時常見的 DOM 跳動(Jank)與 Key 綁定問題。

Human vs. AI:提示詞與協同開發

Framer Motion 的語法非常直覺,但要做出「彈性恰到好處(Spring Physics)」而非「生硬遲鈍」的動畫,參數調整才是精髓所在。

給 AI 的指令(Prompt)

我們正在 Next.js + Tailwind CSS 專案中調整 UI 質感。
請幫我用 Framer Motion 重構交易紀錄列表元件 TransactionList.tsx。

要求:

  1. 列表載入時,每一項交易卡片要有「交錯進場(Staggered Animation)」效果,由下往上淡入。
  2. 當使用者懸停(Hover)在卡片上時,卡片要有輕微的微浮(Hover Lift)與陰影加深效果;點擊(Tap)時有微小縮放(Scale Down)反饋。
  3. 當刪除某筆交易時,被刪除的卡片需要流暢地淡出縮小並消失,其餘卡片平滑補位。
  4. 使用 Spring 物理動畫,讓體感自然流暢。

AI 產出的結果

AI 回得非常迅速,幾秒鐘就寫好了一個套用 motion.div 與 AnimatePresence 的元件:

// src/components/TransactionList.tsx
'use client';

import { motion, AnimatePresence } from 'framer-motion';
import { Transaction } from '@/hooks/useTransactions';

export const TransactionList = ({ transactions }: { transactions: Transaction[] }) => {
  return (
    <div className="space-y-3">
      <AnimatePresence>
        {transactions.map((tx) => (
          <motion.div
            key={tx.id}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
            whileHover={{ scale: 1.02 }}
            whileTap={{ scale: 0.98 }}
            className="p-4 bg-slate-800 rounded-xl flex justify-between items-center shadow-md"
          >
            <div>
              <p className="font-semibold text-white">{tx.note}</p>
              <p className="text-sm text-slate-400">{tx.category}</p>
            </div>
            <span className={tx.type === 'EXPENSE' ? 'text-rose-400' : 'text-emerald-400'}>
              {tx.type === 'EXPENSE' ? '-' : '+'}${tx.amount}
            </span>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
};

翻車與除錯過程 (Debug Experience)

將這段 Code 放上頁面測試時,視覺效果雖然看起來「動起來了」,但細看卻有很多令人不適的 UI 瑕疵:

哪裡有錯?

  1. 刪除時卡片「硬跳」沒有平滑補位 (Layout Shift):
    當刪除中間某一筆交易時,雖然該卡片成功淡出了,但下方卡片卻是「硬生生直接跳上來」,完全沒有平滑補位的動畫!這是因為 AI 忘記給 motion.div 加上 Framer Motion 靈魂屬性:layout。

  2. 缺乏「交錯 (Stagger)」體感,百萬卡片齊刷刷彈出:
    AI 雖然收到了「交錯進場」的指令,但它直接把 initial 和 animate 寫死在子卡片上,沒有使用 variants 搭配 staggerChildren,導致所有列表卡片在同一個時間點瞬間彈出,視覺非常雜亂。

  3. Hover 導致邊框模糊與內容抖動:

  4. whileHover={{ scale: 1.02 }} 雖然實現了放大,但直接改變 scale 會導致向量字體與內部邊框在特定螢幕解析度下出現模糊(Subpixel Rendering Bug)。

修正與引導(Human Decision)

將問題整理成標準的第二輪診斷指令:

修正 Prompt:
「1. 請改用 variants 父子架構,在父層容器定義 staggerChildren: 0.08,實現流暢的交錯入場。
2. 請在子卡片 <motion.div> 加上 layout 屬性,讓列表增刪時周圍元件能平滑滑動補位。
3. 將 whileHover 從 scale: 1.02 改為以 y: -2 與 shadow 變化為主,避免字體模糊。」

最終修正版本 (Refactored Code)

經過二輪優化後的最終版程式碼,不僅性能優異,質感更是直接媲美頂級 SaaS 產品:

'use client';

import { motion, AnimatePresence } from 'framer-motion';
import { Transaction } from '@/hooks/useTransactions';

// 父層容器:控制子元素的交錯進場時間
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.06, // 每項間隔 0.06 秒
    },
  },
};

// 子卡片動畫:物理彈簧體感
const itemVariants = {
  hidden: { opacity: 0, y: 15 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { type: 'spring', stiffness: 300, damping: 24 },
  },
  exit: {
    opacity: 0,
    scale: 0.95,
    transition: { duration: 0.2 },
  },
};

interface Props {
  transactions: Transaction[];
  onDelete?: (id: string) => void;
}

export const TransactionList = ({ transactions, onDelete }: Props) => {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-3"
    >
      <AnimatePresence mode="popLayout">
        {transactions.map((tx) => (
          <motion.div
            key={tx.id}
            layout //  關鍵:讓列表變動時,周圍卡片自動平滑位移
            variants={itemVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            whileHover={{ y: -2, boxShadow: '0px 10px 20px rgba(0, 0, 0, 0.2)' }}
            whileTap={{ scale: 0.98 }}
            className="p-4 bg-slate-800/80 backdrop-blur border border-slate-700/50 rounded-xl flex justify-between items-center transition-colors hover:border-slate-600"
          >
            <div>
              <p className="font-medium text-slate-100">{tx.note}</p>
              <p className="text-xs text-slate-400 mt-0.5">{tx.category}</p>
            </div>
            <div className="flex items-center gap-3">
              <span className={`font-semibold ${tx.type === 'EXPENSE' ? 'text-rose-400' : 'text-emerald-400'}`}>
                {tx.type === 'EXPENSE' ? '-' : '+'}${tx.amount.toLocaleString()}
              </span>
              {onDelete && (
                <button
                  onClick={() => onDelete(tx.id)}
                  className="text-slate-500 hover:text-slate-300 text-xs px-2 py-1 rounded transition-colors"
                >
                  刪除
                </button>
              )}
            </div>
          </motion.div>
        ))}
      </AnimatePresence>
    </motion.div>
  );
};

JavaScript / 前端筆記

FLIP 動畫原理與 layout 屬性:
Framer Motion 的 layout 屬性底層使用了 FLIP (First, Last, Invert, Play) 繪圖模式。它會在 DOM 改變前後計算位置差,並用高效率的 transform 補齊過渡動畫,避免瀏覽器觸發昂貴的 Reflow(重排)。

AnimatePresence mode="popLayout":
在 React 中,當元件被 Unmount(從 DOM 移除)時,預設是無法播放離場動畫的。AnimatePresence 能暫存即將消失的 DOM,而 mode="popLayout" 則能防止離場元素佔用排版空間,解決卡片刪除時的卡頓感。


上一篇
# Day 16 - 動態資料視覺化:生成 Recharts / Chart.js 圖表組件
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言